一个例子教你学会DialogFragment —模仿国际微博评论框
用 DialogFragment 模仿国际微博评论框。
showzeng的博客地址:
http://showzeng.itscoder.com/android/2017/08/11/the-imitation-of-the-international-weibo-comment-box.html
前言:在写家园内部办公系统 US APP 话题页面的时候,因为自己绞尽脑汁也想不出什么好的设计图,脑海中唯一印象深刻的就是国际微博的 UI 了,索性就模仿着来吧,于是有了本文。
啥也不说了,先上图
分析
1.从图中,可以看出,微博详情页底下的一个评论条其实并不做输入框使用,而是充当一个 Button 的作用,唤起真正的输入框。
2.真正的输入框,从效果来看,像是 Dialog,这里选用了自定义布局的 DialogFragment。
3.输入框 Dismiss 或者 Cancel 时,若评论并未提交,则将其显示到底部的评论条中,单行显示,溢出省略。
4.再次进入输入框时,若评论条内容不为空,需将文本内容填充到输入框中。
5.评论框中无字符输入时,提交按钮为灰色且不可触发,反之为正常的提交按钮。
分析完了那就开工吧
1. 底部评论条
这个就比较简单了,一个 LinearLayout 包裹住头像控件和文本控件且置于屏幕底部。这里想要提出的一点:
Tip 1: 直接选用 TextView 而不是 EditText
在一开始的时候,我的第一直觉是认为那是一个 EditText,所以直接用的这个控件,设置为不可编辑,一切都看似那么的合乎情理。直到调试的时候,我发现在首次进入页面的时候,第一次点击评论条,并不能触发点击事件,需要第二次点击时才有效,而后就不会再出现问题。我试图去找过是什么原因,但未果,也不排除是否是机型或者什么其他原因,但倾向于是控件的事件分发哪里被截断了。总之,如果你能找到原因,还请告知,让我学习学习,感激不尽。
之后转念一想,反正 TextView 也是可以设置 Hint 的,那为啥不直接用 TextView 呢,简单省事。
下面是布局代码:
<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_my_material_grey"
android:orientation="vertical">
......
</RelativeLayout>
2. DialogFragment 评论框
评论框布局也是比较简单的,一个无背景的输入框,一个提交按钮,以及左侧几个附加的功能按钮,比如选图,和 @ 功能,这个就直接上布局文件了:
<!-- dialog_fragment_comment_layout.xml -->
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="130dp"
android:layout_gravity="bottom"
android:background="@color/background_my_material_light">
......
</RelativeLayout>
3. DialogFragment 逻辑代码
关于 DialogFragment 用法,网上搜一下就有很多了。首先必须要实现的是 onCreateView() 或者是 onCreateDialog() 方法,这里比较核心的应该就是布局的位置了,因为 DialogFragment 默认布局是常见的 Dialog 样式,这里需要手动设置:
// CommentDialogFragment.java
public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{
private Dialog mDialog;
private EditText commentEditText;
private ImageView photoButton;
private ImageView atButton;
private ImageView sendButton;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// 自定义 style BottomDialog
mDialog = new Dialog(getActivity(), R.style.BottomDialog);
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
mDialog.setContentView(R.layout.dialog_fragment_comment_layout);
// 外部点击设置为可以取消
mDialog.setCanceledOnTouchOutside(true);
Window window = mDialog.getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
// 布局属性位于整个窗口底部
layoutParams.gravity = Gravity.BOTTOM;
// 布局属性宽度填充满整个窗口宽度,默认是有 margin 值的
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
window.setAttributes(layoutParams);
commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);
photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);
atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);
sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);
photoButton.setOnClickListener(this);
atButton.setOnClickListener(this);
sendButton.setOnClickListener(this);
return mDialog;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.image_btn_photo:
Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();
break;
case R.id.image_btn_at:
Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();
break;
case R.id.image_btn_comment_send:
Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();
dismiss();
break;
default:
break;
}
}
}
自定义的 BottomDialog style:
<!-- styles.xml -->
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
...
</style>
<style name="BottomDialog" parent="@style/AppTheme">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:windowIsFloating">true</item>
</style>
</resources>
4. MainActivity 逻辑代码
此时只需要为评论条添加监听器,响应 DialogFragment 的呼出即可:
// MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private TextView commentFakeButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
commentFakeButton = (TextView) findViewById(R.id.tv_comment_fake_button);
commentFakeButton.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_comment_fake_button:
CommentDialogFragment commentDialogFragment = new CommentDialogFragment();
commentDialogFragment.show(getFragmentManager(), "CommentDialogFragment");
break;
default:
break;
}
}
}
此时,运行程序,大体上是没问题了,接下来就需要实现它的一些小 Feature,一个是评论文本内容填充,一个是评论框文本输入监听,控制评论提交按钮的状态。
5. 评论文本内容填充
这里第一反应可能是普通 Activity 与 Fragment 间的通信,之后在网上看文章时,学习到回调这种用法,也是第一次比较深入地去理解回调这门艺术,不得不说,回调真是个好东西。接下来我们就来看看怎么用回调来实现这一个 Feature。
首先需要定义一个接口:
// DialogFragmentDataCallback.java
public interface DialogFragmentDataCallback {
String getCommentText();
void setCommentText(String commentTextTemp);
}
代码很简单,这里比较好的理解方式是,一个类实现这个接口,那么就必须重写这两个方法,而 get 方法,可以看作是提供给外部类调用,用来获取这个类的一些数据,而 set 方法,可看作是外部类用来回调操作此类的一个暴露的方法。那接下来就让 MainActivity 实现这个接口:
// MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener, DialogFragmentDataCallback{
private TextView commentFakeButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
commentFakeButton = (TextView) findViewById(R.id.tv_comment_fake_button);
commentFakeButton.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_comment_fake_button:
CommentDialogFragment commentDialogFragment = new CommentDialogFragment();
commentDialogFragment.show(getFragmentManager(), "CommentDialogFragment");
break;
default:
break;
}
}
@Override
public String getCommentText() {
// 提供给外部类获取本类中数据的方法,这里就是用于获取评论条的文本,将其返回给外部类就可以
return commentFakeButton.getText().toString();
}
@Override
public void setCommentText(String commentTextTemp) {
// 提供给外部类用来回调设置评论条文本内容的方法
commentFakeButton.setText(commentTextTemp);
}
}
接着在 CommentDialogFragment 中实现文本的填充:
// CommentDialogFragment.java
public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{
private Dialog mDialog;
private EditText commentEditText;
private ImageView photoButton;
private ImageView atButton;
private ImageView sendButton;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mDialog = new Dialog(getActivity(), R.style.BottomDialog);
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
mDialog.setContentView(R.layout.dialog_fragment_comment_layout);
mDialog.setCanceledOnTouchOutside(true);
Window window = mDialog.getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.gravity = Gravity.BOTTOM;
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
window.setAttributes(layoutParams);
commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);
photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);
atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);
sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);
// 填充文本方法
fillEditText();
photoButton.setOnClickListener(this);
atButton.setOnClickListener(this);
sendButton.setOnClickListener(this);
return mDialog;
}
private void fillEditText() {
// 获取 MainActivity 实例并转型为 DialogFragmentDataCallback 接口
dataCallback = (DialogFragmentDataCallback) getActivity();
// 回调获取评论条文本内容并填充至输入框中
commentEditText.setText(dataCallback.getCommentText());
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.image_btn_photo:
Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();
break;
case R.id.image_btn_at:
Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();
break;
case R.id.image_btn_comment_send:
Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();
// 提交成功后 ( 根据具体需求一般为网络请求发送评论 ) 清空评论框, 重写 onDismiss,使评论框文本内容填充回评论条,这里提交后就设为空
commentEditText.setText("");
dismiss();
break;
default:
break;
}
}
@Override
public void onDismiss(DialogInterface dialog) {
// 重写 onDismiss,将评论框文本填充回评论条
dataCallback.setCommentText(commentEditText.getText().toString());
super.onDismiss(dialog);
}
@Override
public void onCancel(DialogInterface dialog) {
// 重写 onCancel,将评论框文本填充回评论条,一般是点击外部及返回键所触发
dataCallback.setCommentText(commentEditText.getText().toString());
super.onCancel(dialog);
}
}
6. 评论框文本输入监听,控制评论提交按钮的状态
这个算是本文比较有趣的一部分,也是自己在实现这个项目效果中碰到坑最多的地方了,让我细细说来。首先是 EditText 的输入监听,使用的是 TextWatcher,当输入的文本字符大于 0 时,将 sendButton 禁用,设置颜色为灰色。
Tip 2: 在刚进入输入框,未执行动作前,TextWatcher 是无法监听此时状态的
因为刚开始进入输入框时,在还未执行输入动作之前,TextWatcher 无法监听此时的状态,所有方法都得不到调用。所以,在填充文本时,就应该要做一个判断,因为 ImageView 充当 Button 默认设置的是可点击的,所以在文本为空时,需要将发送按钮禁用。
Tip 3: ImageView 充当 Button,clickable 属性设置无效,应使用 enable 属性
在一开始时,我是试图用 clickable 属性来控制这个 ImageView 假 Button 的点击事件,但却发现这个属性完全失效不起作用。不是说 ImageView 默认是不可点击的吗?那我先在 xml 里设置 clickable 属性为 false 也是没用的,后来经大佬提点,才知道是监听器的原因。为 ImageView 设置 OnClickListener 时,会自动将其属性 clickable 设置为 true,这点请看源码:
// View.java
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
*
* @param l The callback that will run
*
* @see #setClickable(boolean)
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
既然如此,那就在代码中设置其 enable 属性,将其完全禁用:
// CommentDialogFragment.java
public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{
private Dialog mDialog;
private EditText commentEditText;
private ImageView photoButton;
private ImageView atButton;
private ImageView sendButton;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mDialog = new Dialog(getActivity(), R.style.BottomDialog);
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
mDialog.setContentView(R.layout.dialog_fragment_comment_layout);
mDialog.setCanceledOnTouchOutside(true);
Window window = mDialog.getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.gravity = Gravity.BOTTOM;
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
window.setAttributes(layoutParams);
commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);
photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);
atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);
sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);
fillEditText();
// 为 EditText 设置监听器
commentEditText.addTextChangedListener(mTextWatcher);
photoButton.setOnClickListener(this);
atButton.setOnClickListener(this);
sendButton.setOnClickListener(this);
return mDialog;
}
private void fillEditText() {
dataCallback = (DialogFragmentDataCallback) getActivity();
commentEditText.setText(dataCallback.getCommentText());
// 若评论条文本内容为空,禁用按钮
if (dataCallback.getCommentText().length() == 0) {
sendButton.setEnabled(false);
sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));
}
}
// TextWatcher,主要重写 afterTextChanged 方法
private TextWatcher mTextWatcher = new TextWatcher() {
private CharSequence temp;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
temp = s;
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
// 输入动作触发后,将进行监听,若文本被清空,禁用按钮,反之,则恢复正常
if (temp.length() > 0) {
sendButton.setEnabled(true);
sendButton.setClickable(true);
sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.colorAccent));
} else {
sendButton.setEnabled(false);
sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));
}
}
};
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.image_btn_photo:
Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();
break;
case R.id.image_btn_at:
Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();
break;
case R.id.image_btn_comment_send:
Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();
commentEditText.setText("");
dismiss();
break;
default:
break;
}
}
@Override
public void onDismiss(DialogInterface dialog) {
dataCallback.setCommentText(commentEditText.getText().toString());
super.onDismiss(dialog);
}
@Override
public void onCancel(DialogInterface dialog) {
dataCallback.setCommentText(commentEditText.getText().toString());
super.onCancel(dialog);
}
}
7. 在弹出评论框时呼出软键盘
关于软键盘的呼出问题,也是一个比较头疼的问题,自己也碰到了比较多坑,网上的解决方案比较多,也比较多没有用 :) 。文末有推荐几篇比较优秀的文章可供参考。
Tip 4: 在给 EditText 获取焦点 (requestFocus() 方法) 之前,需设置 setFocusable 和 setFocusableInTouchMode 为 true
第一个要提出的点是:在填充评论条文本内容到评论框后,需要调用 EditText 的 setSelection 方法设置光标位置,不然魅族默认没有光标,也不会有软键盘弹出,我不知道是不是只有我的手机会这样,反正用另一部华为手机就没有这个问题。( 这里有点奇怪的是,为什么国际微博评论框弹出时,光标是置于文本头的,感觉这里很不人性化 )
第二个是软键盘的弹出,之前就是怎么都弹不出来,之后在一篇文章中看到说设置延时是很关键的一步,我试过之后,发现确实如此,可是所有文章都未提到为什么一定要设置延时,唯一说到一点的是说软键盘的加载需要一点时间,当然,我仍然不知道这里为何,这个理由完全不是需要延时的原因啊。延时时间是我手动调试过去的,但发现偶尔还是弹不出来,这里实在是不解,而且不同手机又怎么保证延时时间是正确的?如果你知道这个问题的答案,还请告知,让我学习学习,感激不尽。下面是 CommentDialogFragment 完整的代码:
// CommentDialogFragment.java
public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{
private Dialog mDialog;
private EditText commentEditText;
private ImageView photoButton;
private ImageView atButton;
private ImageView sendButton;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mDialog = new Dialog(getActivity(), R.style.BottomDialog);
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
mDialog.setContentView(R.layout.dialog_fragment_comment_layout);
mDialog.setCanceledOnTouchOutside(true);
Window window = mDialog.getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.gravity = Gravity.BOTTOM;
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
window.setAttributes(layoutParams);
commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);
photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);
atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);
sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);
fillEditText();
// 呼出软键盘方法
setSoftKeyboard();
commentEditText.addTextChangedListener(mTextWatcher);
photoButton.setOnClickListener(this);
atButton.setOnClickListener(this);
sendButton.setOnClickListener(this);
return mDialog;
}
private void fillEditText() {
dataCallback = (DialogFragmentDataCallback) getActivity();
commentEditText.setText(dataCallback.getCommentText());
// Write this line for meizu 特别的爱,给特别的你
commentEditText.setSelection(dataCallback.getCommentText().length());
if (dataCallback.getCommentText().length() == 0) {
sendButton.setEnabled(false);
sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));
}
}
private void setSoftKeyboard() {
// 为 EditText 获取焦点
commentEditText.setFocusable(true);
commentEditText.setFocusableInTouchMode(true);
commentEditText.requestFocus();
// TODO: 17-8-11 为何这里要延时才能弹出软键盘, 延时时长又如何判断? 目前是手动调试
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.toggleSoftInput(0, InputMethodManager.SHOW_FORCED);
}
}, 110);
}
private TextWatcher mTextWatcher = new TextWatcher() {
private CharSequence temp;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
temp = s;
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (temp.length() > 0) {
sendButton.setEnabled(true);
sendButton.setClickable(true);
sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.colorAccent));
} else {
sendButton.setEnabled(false);
sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));
}
}
};
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.image_btn_photo:
Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();
break;
case R.id.image_btn_at:
Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();
break;
case R.id.image_btn_comment_send:
Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();
commentEditText.setText("");
dismiss();
break;
default:
break;
}
}
@Override
public void onDismiss(DialogInterface dialog) {
dataCallback.setCommentText(commentEditText.getText().toString());
super.onDismiss(dialog);
}
@Override
public void onCancel(DialogInterface dialog) {
dataCallback.setCommentText(commentEditText.getText().toString());
super.onCancel(dialog);
}
}
写在最后
本文项目完整代码已提交到 GitHub 上,如有需要,请自取。PureComment 项目链接
本文到这里差不多就结束了,感谢阅读。关于字数限制和输入字符数量提示,这个在 TextWatcher 里监听就可以实现了,因为自己项目中评论并没有字数限制,所以这里没有写这个部分,不过相信讲到这里,自己去实现也是很简单的啦!可参考下面给出的参考文档。
最后,如果你有更好的实现方法或者什么建议,欢迎联系,一起讨论交流。Thank you and have a nice day!
扩展阅读,参考文档
Android 官方推荐 : DialogFragment 创建对话框
DialogFragment 实现底部弹窗
DialogFragment 生命周期简介
知乎:回调函数(callback)是什么?
onTextChanged 参数解释及实现 EditText 字数监听
Android 软键盘隐藏寻找最优解
demo项目地址::
https://github.com/showzeng/PureComment
终端研发部提倡: 没有做不到的,只有想不到的。
在这里获得的不仅仅是技术!
这里学到不仅仅是技术